pacenotes_installer\wizard\scene/
installation.rs1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5pub unsafe extern "system" fn installation_page(
9 dialog: HWND,
10 message: u32,
11 wparam: WPARAM,
12 lparam: LPARAM,
13) -> isize {
14 match message {
15 wm::WM_INITDIALOG => {
16 crate::wizard::page::adopt_state(dialog, lparam);
17 super::progress_page::init_progress(dialog);
18 1
19 }
20 crate::worker::WM_WORKER_PROGRESS => {
21 let Some(progress) = crate::worker::take_progress(lparam) else {
22 return 0;
23 };
24 if let Some(state) = crate::wizard::page::state(dialog) {
25 if state.is_current_request(wparam) {
26 super::progress_page::show_progress(dialog, state, &progress);
27 }
28 }
29 1
30 }
31 crate::worker::WM_WORKER_DONE => {
32 let Some(message) = crate::worker::take(lparam) else {
33 return 0;
34 };
35 if let (Some(state), crate::worker::Message::Installed(outcome)) =
36 (crate::wizard::page::state(dialog), message)
37 {
38 if state.is_current_request(wparam) {
39 finish(dialog, state, outcome);
40 }
41 }
42 1
43 }
44 wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
45 Some(ctl::PSN_SETACTIVE) => {
46 crate::wizard::page::set_sheet_caption(dialog);
47 crate::wizard::page::set_header(dialog, crate::i18n::IDS_S6_TITLE);
48 crate::wizard::page::set_buttons(dialog, 0);
49 crate::ui::enable(
50 crate::wizard::page::sheet(dialog),
51 crate::ui::IDCANCEL,
52 false,
53 );
54 if let Some(state) = crate::wizard::page::state(dialog) {
55 super::progress_page::show_log(dialog, state);
56 }
57 start(dialog);
58 crate::wizard::page::reply(dialog, 0)
59 }
60 Some(ctl::PSN_QUERYCANCEL) => super::progress_page::on_cancel(dialog),
61 _ => 0,
62 },
63 _ => 0,
64 }
65}
66
67unsafe fn start(dialog: HWND) {
70 let Some(state) = crate::wizard::page::state(dialog) else {
71 return;
72 };
73 if state.install_started {
74 return;
75 }
76 state.install_started = true;
77 if state.route == crate::wizard::state::Route::Restore {
78 start_restore(dialog, state);
79 return;
80 }
81
82 for key in state.config.install_keys() {
83 super::progress_page::log_line(dialog, state, &format!("selected: {key}"));
84 }
85 if !confirm_removals(dialog, state) {
87 finish(dialog, state, crate::worker::Outcome::Cancelled);
88 return;
89 }
90
91 crate::ui::set_text(
92 dialog,
93 crate::wizard::control_ids::IDC_S6_PHASE,
94 crate::i18n::IDS_S6_CHECKING,
95 );
96 let request = state.next_request();
97 let job = crate::worker::InstallJob {
98 base_url: state.base_url.clone(),
99 cache_root: state.cache.root().to_path_buf(),
100 game_dir: std::path::PathBuf::from(&state.folder),
101 game: state
102 .game
103 .unwrap_or(pacenotes_installer::gamedir::Game::Rbr),
104 selection: state.config.install_keys(),
105 picks: state.config.picks_by_category(),
106 choice_categories: choice_categories(state),
107 facts: host_facts(state),
108 cancel: std::sync::Arc::clone(&state.cancel),
109 };
110 crate::worker::spawn_reporting(dialog, request, move |reporter| {
111 crate::worker::run_install(job, reporter)
112 });
113}
114
115unsafe fn start_restore(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
118 let Some(point) = state.restore_to.clone() else {
119 finish(dialog, state, crate::worker::Outcome::Cancelled);
120 return;
121 };
122 let plan = &state.restore_plan;
123 let lines: Vec<String> = std::iter::once(format!("restoring to {}", point.name))
124 .chain(plan.remove.iter().map(|key| format!("to remove: {key}")))
125 .chain(
126 plan.put_back
127 .iter()
128 .map(|wanted| format!("to put back: {}", wanted.key)),
129 )
130 .collect();
131 for line in &lines {
132 super::progress_page::log_line(dialog, state, line);
133 }
134
135 crate::ui::set_text(
136 dialog,
137 crate::wizard::control_ids::IDC_S6_PHASE,
138 crate::i18n::IDS_RESTORE_RUNNING,
139 );
140 let request = state.next_request();
141 let job = crate::worker::RestoreJob {
142 cache_root: state.cache.root().to_path_buf(),
143 game_dir: std::path::PathBuf::from(&state.folder),
144 game: state
145 .game
146 .unwrap_or(pacenotes_installer::gamedir::Game::Rbr),
147 target: point.snapshot,
148 plan: state.restore_plan.clone(),
149 choice_categories: choice_categories(state),
150 facts: host_facts(state),
151 };
152 crate::worker::spawn_reporting(dialog, request, move |reporter| {
153 crate::worker::run_restore(job, reporter)
154 });
155}
156
157fn choice_categories(state: &crate::wizard::state::WizardState) -> Vec<String> {
158 pacenotes_installer::schema::choice_categories(state.game_id())
159 .iter()
160 .map(|category| category.to_string())
161 .collect()
162}
163
164unsafe fn host_facts(
166 state: &crate::wizard::state::WizardState,
167) -> pacenotes_installer::engine::HostFacts {
168 pacenotes_installer::engine::HostFacts {
169 free_bytes: free_space(&state.folder),
170 game_running: state
171 .game
172 .is_some_and(|game| crate::game_process::is_running_from(game, &state.folder)),
173 }
174}
175
176unsafe fn confirm_removals(dialog: HWND, state: &mut crate::wizard::state::WizardState) -> bool {
179 let selected = state.config.install_keys();
180 let going: Vec<String> = pacenotes_installer::ledger::Ledger::open(&state.folder)
181 .packages()
182 .keys()
183 .filter(|key| !selected.contains(key))
184 .cloned()
185 .collect();
186 if going.is_empty() {
187 return true;
188 }
189 let question =
190 crate::i18n::load_string_arg(crate::i18n::IDS_S6_CONFIRM_REMOVE, &going.join("\n"));
191 if crate::ui::confirm(dialog, &question) {
192 return true;
193 }
194 super::progress_page::log_line(dialog, state, "the user declined to remove anything");
195 false
196}
197
198unsafe fn free_space(folder: &str) -> Option<u64> {
200 let wide = crate::i18n::to_wide(folder);
201 let mut free: u64 = 0;
202 let measured = windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
203 wide.as_ptr(),
204 &mut free,
205 core::ptr::null_mut(),
206 core::ptr::null_mut(),
207 );
208 (measured != 0).then_some(free)
209}
210
211unsafe fn finish(
213 dialog: HWND,
214 state: &mut crate::wizard::state::WizardState,
215 outcome: crate::worker::Outcome,
216) {
217 let line = match &outcome {
218 crate::worker::Outcome::Done { applied } => format!(
219 "finished: {} installed, {} removed",
220 applied.installed.len(),
221 applied.uninstalled.len()
222 ),
223 crate::worker::Outcome::Failed(message) => format!("failed: {message}"),
224 crate::worker::Outcome::Cancelled => "cancelled by the user".to_string(),
225 };
226 super::progress_page::log_line(dialog, state, &line);
227 if let crate::worker::Outcome::Done { applied } = &outcome {
228 if let Some(point) = &applied.restore_point {
229 super::progress_page::log_line(dialog, state, &format!("restore point: {point}"));
230 }
231 }
232 state.outcome = Some(outcome);
233 state.cache = pacenotes_installer::cache::Cache::open(state.cache.root().to_path_buf());
234 super::progress_page::finish_page(dialog, crate::i18n::IDS_S6_FINISHED, None);
235}